# -*- mode: python; coding: utf-8; -*-
##--------------------------------------------------------------------------##
## NPSolitaire:
## Copyright (C) 2010-2013 Jeremy Austin-Bardo <tjaustinbardo@gmail.com>
##
## from PySolFC code:
## Copyright (C) 1998-2003 Markus Oberhumer <markus@oberhumer.com>
## Copyright (C) 2003 Mt. Hood Playing Card Co.
## Copyright (C) 2005-2010 Skomoroh <skomoroh@users.sourceforge.net>
##
## This program is free software: you can redistribute it and/or modify
## it under the terms of the GNU General Public License as published by
## the Free Software Foundation, version 3 of the License.
##
## This program is distributed in the hope that it will be useful,
## but WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see <http://www.gnu.org/licenses/>.
##
##--------------------------------------------------------------------------##
"""
Game class with mixins that creates, manages and records a solitaire card game.
"""
__docformat__ = 'restructuredtext en'
#_ = lambda n: n

import time     # for sleep method
import logging

LOGGER = logging.getLogger('PySolFC.GameLib')

from ..animation import animate_redeal
from ..pysolaudio import AUDIO_DEV
from ..settings import TITLE, CONF_OPT
from ..statdb import STAT_DB
from ..utillib.misc import Timer as GameTimer, Size, Scale
from ..viewlib.console import update_console  #, STATUS_BAR
from ..viewlib.constants import CURSOR_WATCH
from ..viewlib.dialog import Dialog
from . import state
from .info import GT
from .bind import GameBind
from .demo import GameDemo
from .drag import GameDrag
from .move import GameMove
from .pack import GamePack
from .store import GameStore

TIMER_UPDATE = 200


class Game(GamePack, GameStore, GameMove, GameDemo, GameDrag, GameBind):
    """
    Base class for all solitaire games
    """

    def __init__(self, gameinfo):
        """
        Initialize game details.

        Set initial state for attributes. Call next method in MRO.

        *Parameters*:
          gameinfo : {Map}
            gameinfo data from GAME_DB set by Game details itselfouter App loop.

        *Variables*:
          self.stackmap : {dict}
            Empty map of id int value at coords tuple key set by
            *Stack.__init__()* to locate by location.
          self.allstacks : {list}
            Empty list of *Stack* set by *Stack.__init__()* to locate by id.
          self.s : {Map}
            Empty map of common stacks type keys -- talon, waste, foundations,
            rows, reserves, xwastes, braid, internals and their stack object /
            object list values. Set during create_game().
          self.sg : {Mapping}
            Empty map of common stacks groups for drag stack, hint moves, auto
            play, amd stack highlight. Set during create_game() and adjusted
            during _pile_prepare().
        """
        self.__info__ = gameinfo
        super(Game, self).__init__()
        self.stackmap = {}
        self.allstacks = []
        # common stack types
        self.s = dict(talon=None, wastes=[], foundations=[], rows=[],
                        reserves=[], braid=[], internals=[])
        # common stack groups
        self.sg = {'openstacks': [],  # buildable piles with get_closest_stack()
                   # hint.AbstractHint based hints discover new moves
                   'Talons': [], 'reservestacks': [],
                   # hint.AbstractHint based hints discover new moves
                   # and _autopiles for flip, drop, and quickplay
                   'dropstacks': [],
                   # _hightlight_piles property for piles to highlight
                   'hp_stacks': []}
        self.top = self.nextgame = self.canvas = None
        self.win_animation = None
        self.__init_view_size = Size(0, 0)
        self.__init_text_coords = {}
        self._timer = GameTimer()
        self.is_preview = self.busy = self.is_pause = False
        self.play_timer = None

    def __getitem__(self, key):
        """
        Get Game Details by key.

        Mirror Game detail functionality.

        .. Game[key] -> *
        """
        return self.__info__.get(key, None)

    @property
    def width(self):
        """
        Return inital table width for game.

        *note:* This is inital table width required by layout not window size.

        .. width -> int
        """
        return self.__init_view_size.w

    @property
    def height(self):
        """
        Return inital table height for game.

        *note:* This is inital table height required by layout not window size.

        .. height -> int
        """
        return self.__init_view_size.h

    @property
    def table_size(self):
        """
        Return current table size for game.

        *note:* This is scaled table size required by layout not window size.

        .. table_size -> Size
        """
        scale_ = Scale(CONF_OPT['scale_x'], CONF_OPT['scale_y'])
        return self.__init_view_size._scale(scale_)

    @table_size.setter
    def table_size(self, value):
        """
        Set inital table size for game.

        Given value should be a Size tuple of width and height that includes
        entire visable layout. It should be called by _create_game().

        *note:* This is intial table size required by layout not window size.

        .. table_size = Size

        *Parameters*:
          value : {Size}
        """
        self.__init_view_size = Size(int(value[0]), int(value[1]))

    def reset(self, restart=False):
        """
        Reset current game.

        Call next method in order. Reset timer and win animation. Called by
        new and restore methods.

        .. reset([restart:bool=False]):

        *Keywords*:
          restart : {bool}
            Allow some attributes to survive a reset, but not a restart.
        """
        ancestor = super(Game, self)
        if hasattr(ancestor, 'reset'):
            ancestor.reset(restart)
        self._timer(restart)
        if not restart:
            self.win_animation = None  # reset main win animation object

    @state.game_busy
    def _pile_prepare(self):
        """
        Prepare stack data for game.

        Confirm pile goup details. Sequences in *s* and *sg* are optimized as
        tuples. Each pile in allstacks sequene has their prepare and assert
        method called. Confirm talon has rounds and max_rounds attrs.

        .. _pile_prepare() -> None

        *Variables*:
          self.allstacks : {tuple}
            Optimize listing for speed with tuple.
          self.s : {Maping}
            Optimize each value listing for speed with tuples.
          self.sg['openstacks'] : {tuple}
            Confirm Stack listing can take cards.
          self.sg['hpstacks'] : {tuple}
            Confirm Stack listing sg['dropstacks'] can move cards.

        *Raise Error*:
          AssertionErrors for any stack that fails their assertStack() method
          and talon lacks required attrs.
        """
        pile, group = self.s, self.sg
        group['openstacks'] = tuple(p for p in group['openstacks']
                                  if p.cap['max_accept'] >= p.cap['min_accept'])
        group['hp_stacks'] = tuple(p for p in group['dropstacks']
                                     if p.cap['max_move'] >= 2)
        # convert stackgroups to tuples (speed)
        self.allstacks = tuple(self.allstacks)
        for key in ('wastes', 'foundations', 'rows', 'reserves', 'braid',
                    'internals'):
            assert isinstance(pile[key], (list, tuple))
            pile[key] = tuple(pile[key])
        for key in ('Talons', 'dropstacks', 'reservestacks'):
            assert isinstance(group[key], (list, tuple))
            pile[key] = tuple(group[key])
        # init the stack view
        for cpile in self.allstacks:
            cpile.prepareStack()
            cpile.assertStack()
        if pile['talon']:
            assert all(hasattr(pile['talon'], t)
                       for t in ('round', 'max_rounds',))

    @state.game_busy
    def prepare(self, app, preview=False):
        """
        Set app attributes on game. Call next method in order. Init text
        mappings for canvas text. Create layout and card/tile pack(s) method.
        Prepare and optimize piles and their regions. When not preview set
        additional key/mouse binding, prepare canvas size and reset status help.

       *Parameters*:
          app : {Application}
            game application with outer loop and persistent variables.
          preview : {bool} False
           game is within a preview dialog.

        *Variables*:
          self.app : {PySolApp}
            Set to app value passed.
          self.top : {MfxRoot}
            Set to app.top value.
          self.canvas : {ScrolledCanvas}
            Set to app.canvas value.
          self.drag : {Mapping}
            Initialize values for drag stack between stacks.
          self.texts : {Map}
            Initialize values.

        *Notes*:
          Merged common and preview methods into single method
        """
        LOGGER.info('gamename: %s', self['name'])
        self.app = app
        self.top = app.top
        self.nextgame = app.nextgame
        self.canvas = app.canvas
        self.is_preview = preview

        progress = self.app.progress

        ancestor = super(Game, self)
        if hasattr(ancestor, 'prepare'):
            ancestor.prepare(progress)

        self.drag = dict(
            event=None,               # current event
            timer=None,               # current event timer
            start_x=0,                # X coord of initial drag event
            start_y=0,                # Y coord of initial drag event
            stack=None,
            cards=[],
            index=-1,
            shadows=[],               # list of canvas images
            shade_stack=None,         # stack currently shaded
            shade_img=None,           # canvas image
            canshade_stacks=[],       # list of stacks already tested
            noshade_stacks=[],        # for this drag
        )
        # optional canvas.TextItems
        self.texts = {
            'info': None,                # misc info text
            'help': None,                # a static help text
            'misc': None,
            'score': None,               # for displaying the score
            'base_rank': None,           # for displaying the base_rank
            'list': [], }                 # list of additional texts
        # initial position of the texts
        self.__init_text_coords = self.texts.copy()
        if not preview:
            self.top.wm_set_cursor(cursor=CURSOR_WATCH)
            self.top.wm_title(TITLE + ' - ' + self['name'])
            self.top.wm_iconname(TITLE + ' - ' + self['name'])

        # create the game
        if progress:
            progress.update(step=1)

        self._create_game()
        self._pack_create(progress=progress)

        # set some defaults
        self._pile_prepare()
        self._optimize_regions()
        if not preview:
            self.init_bindings()
            self._view_prepare()
            update_console(gamenumber='', moves='', stats='', info='', time='',
                           assist='')
        else:
            self.canvas.set_tablesize(self.table_size)

    @state.game_busy
    def new(self, random=None, restart=False):
        """
        Start a new card game.

        .. new(random:Random=None, restart:bool=False)
             -> None

        *Parameters*:
          random : {Random}
            random object subclass of PysolRandom.
          restart : {bool}
            game reset method. Default False.

        *Variables*:
          self.move_state : {int}
            set S_INIT state
        """
        self.stats_update('restart' if restart else '')
        # 1) Clean up and set inital state
        random = self.random if restart else random
        self.top.wm_set_cursor(cursor=CURSOR_WATCH)
        if self.win_animation is not None:
            self.win_animation.stop()
        if not self.is_preview:
            self.top.event_generate('<<reset_menus>>')
        animate_redeal(self)
        self.reset(restart=restart)
        # 2) Shuffled cards and add to Talon
        self.do_talon_init(random_class=random)
        # 3) Ensure toplevel unhid
        if not self.is_preview:
            self.top.wm_map()
            self.top.update()
        AUDIO_DEV.stop()
        # 4) Deal initial cards to stacks and perform sanity checks
        self.move_state = self.S_INIT
        self._start()
        if self['si']['game_flags'] & GT.OPEN:
            if self.s['talon']:
                assert len(self.s['talon'].cards) == 0
            for pile in (p for p in self.allstacks if p.is_visible):
                for card in pile.cards:
                    assert card.face_up
        self._move_reset()
        # 5) Update interface
        self._view_update()
        # 6) Begin game play
        AUDIO_DEV.stop()
        self.auto_play()
        self.top.wm_set_cursor()
        self._timer_start()

    @state.game_busy
    def restore(self, data, reset=True):
        """
        Restore prior game details.

        Optionally call reset method, before calling next method in order. Set
        finished state. Update view for restored game then start timer.

        .. restore(game:GameData[, reset:bool=True, autoplay:bool=True]) -> None

        *Parameters*:
          data: {GameData}
            Data to restore game details.
          reset: {boolean}
            Reset current game before restore.

        *Variables*:
          finished :
            set to finished of given game
        """
        self.stats_update()
        self.top.wm_set_cursor(cursor=CURSOR_WATCH)
        if reset:
            self.reset()
        ancestor = super(Game, self)
        if hasattr(ancestor, 'restore'):
            ancestor.restore(data, reset, autoplay)
        # 3) Update interface
        self._view_update()
        self.top.after(200, self._config_handler)  # wait for canvas
        if not self.is_preview:
            self.top.wm_map()
            self.top.update()
        # 4) Begin game play
        self.auto_play()
        self.top.wm_set_cursor()
        self._timer_start()

    def nextgame_flags(self, id_, random_class=None):
        """
        Check differences between current and next card game.

        *Parameters*:
          id\_ : {int}
            next game id be loaded

        *Keywords*:
          random_class : {Random}
            random object subclass of *PysolRandom*.

        *Returns*:
          game change differences with id, cardset, seed as int value.
            0 - no change; 1 - id change; 2 - cardset change;
            3 - id and cardset change; 16 - seed change;
            17 - id and seed change; 18 - cardset and seed change
            19 - id, cardset and seed change
        """
        flags = 0
        ancestor = super(Game, self)
        if hasattr(ancestor, 'nextgame_flags'):
            flags = ancestor.nextgame_flags(id_, random_class=random_class)
        if id_ != self['id']:
            flags = flags | 1
        return flags

    @state.not_state('is_preview')
    def quit(self, id_=0, seed=None, data=None, mode=None):
        """
        Quit to outer mainloop.

        .. quit([id_:int=0, random:Random, data:GameData, mode:str]) -> None

        Update stats. Set given values to nextgame map. Pass control to outer
        loop. Depending on map another game may start next or app may quit too.

        Keywords:
          id\_ : {int} 0 (no id)
            Load different game id next game.
          random : {Random} None (new seed)
            Load this random object next game.
          data : {GameData} None
            Load this dumped game next game.
          mode : {str} None ['hold'|'load'|'mark'|'demo'|None]
            mode for outerloop.

        *Variables*:
          self.nextgame : {Map}
            update id, random, data, mode, with passed values.
        """
        self.demo_stop()
        if not self.is_finish:
            self.log()
        self.stats_update('hold' if mode in ['hold', 'mark'] else '')
        self.nextgame['id'] = id_
        self.nextgame['random'] = seed
        self.nextgame['data'] = data
        self.nextgame['mode'] = mode
        update_console(gamenumber='', moves='', stats='', info='', time='',
                       assist='')
        self.top.quit()

    @state.not_state('is_preview')
    def stats_update(self, mode=None):
        """
        Update stats for current game.

        Resume from pause. Update stats when mode given is not hold and restart.
        When mode is restart update nextgame restart counter.

        *note:* This should be called before new, restore, and quit methods.

        .. stats_update(self, mode=None):

        *Parameters:*
          mode : {str[restart|None]}
            next game will start this game over.

        *Variables*:
          self.nextgame['counter']['restart'] : int
            increment value by 1
        """
        self._timer_pause(False)  # Unpause game
        self.top.wm_save_state()
        if mode is None:
            return
        if mode == 'restart':
            if self.move_index > 0 and self.move_player > 0:
                self.nextgame['counter']['restart'] += 1
            return
        if self.changed() and CONF_OPT['update_stats']:
            STAT_DB.update(CONF_OPT['player'], self, status=self.is_success)

    @state.canvas_busy
    def _view_prepare(self):
        """
        Prepare main canvas for possible resize.

        Restore table size. Store initial text coords. Call resize view method.

        .. \_view_prepare():
        """
        # Restore canvas geometry
        if CONF_OPT['auto_scale']:
            self.canvas.set_tablesize(CONF_OPT['game_geometry'])
        else:
            self.canvas.set_tablesize(self.table_size)
            self.top.wm_geometry('')
        # Preserve initial position for self.text items.
        for key, value in self.texts.items():
            if isinstance(value, (list, tuple,)):
                self.__init_text_coords[key] = [getattr(i, 'coord', None)
                                        for i in value]
            else:
                self.__init_text_coords[key] = getattr(value, 'coord', None)
        # Finally resize and reposition elements.
        self._view_resize()
        # Update canvas
        self.top.update_idletasks()

    @state.not_state('is_preview')
    def _view_update(self, mode='all'):
        """
        Update canvas view.

        Call each *Stack._update_text()* then *_update_text()* to update
        canvas labels. Update text on status bar and states on menubar.
        """
        for stack in self.allstacks:
            stack._update_text()
        self._update_text()
        kwargs = {}
        if mode in ['all', 'move']:
            kwargs['moves'] = (self.move_index, self.move_total,)
        if mode in ['all', 'seed']:
            kwargs['gamenumber'] = '#{0}'.format(self.random),
        if mode in ['all', 'stat']:
            kwargs['stats'] = STAT_DB.basic(CONF_OPT['player'], self['id'])
        update_console(**kwargs)
        self.top.event_generate('<<update_menus>>')

    @property
    def _view_scale(self):
        """
        Current scale factor for view.

        .. \_view_scale() -> None
        """
        if CONF_OPT['auto_scale']:
            if self.canvas.winfo_ismapped():  # Apparent canvas size.
                size = self.canvas._cinfo_size
            else:    # No apparent canvas size... use prior geometry.
                size = CONF_OPT['game_geometry']
            # Find scale factor
            factor = [float(s) / i for s, i in zip(size, self.__init_view_size)]
            if CONF_OPT['preserve_aspect_ratio']:
                factor = [min(factor)] * 2
        else:
            factor = CONF_OPT['scale_x'], CONF_OPT['scale_y']
        return Scale(*factor)

    def _view_resize(self):
        """
        Resize game canvas and elements.

        .. \_view_resize() -> None
        """
        self._hide_stack_tip()
        scale = self._view_scale
        LOGGER.debug('resize_view: %s x %s', scale.xscale, scale.yscale)

        ancestor = super(Game, self)
        if hasattr(ancestor, '_view_resize'):
            ancestor._view_resize()

        # 1) scale and position piles
        for pile in self.allstacks:
            pile.resize(scale.x, scale.y)
            pile.updatePositions()

        # 2) position texts
        init_coords = self.__init_text_coords
        for key in ('info', 'help', 'misc', 'score', 'base_rank'):
            coord = init_coords[key]
            if coord:
                self.canvas.coords(self.texts[key], *coord._scale(scale))
        for item, coord in zip(self.texts['list'], init_coords['list']):
            self.canvas.coords(item, *coord._scale(scale))

    @property
    def can_play_sound(self):
        """
        Return play sound for move.

        Default when not S_DEAL or is demo mode.

        .. can_play_sound -> bool
        """
        play_sound = True
        ancestor = super(GameMove, self)
        if hasattr(ancestor, 'can_play_sound'):
            play_sound = ancestor.can_play_sound

        return not self.is_preview and play_sound

    @state.not_state('is_preview')
    def _play_sound(self, name, priority=-1, loop=False):
        """
        Play sound effect.

        .. \play_sound(name:str[, priority:int=-1, loop:bool=False]) -> None

        *Parameters*:
          name : {str}
            name of sound to play
          priority : {int} -1
            sound importance
          loop : {int} False
            sound repeats
        """
        if self.is_preview:
            return
        if CONF_OPT['animations'] and name == 'deal':
            self.canvas.update_idletasks()
        AUDIO_DEV.play(name, priority=priority, loop=loop)

    def _stop_sound(self):
        """
        Stop sound effect.

        .. \_stop_sound() -> None
        """
        AUDIO_DEV.stop()

    def _create_game(self, **kws):
        """
        Create game format.

        Place piles on canvas based on current form and group attributes for
        common talon, wastes, rows, foundations, and reserves.

        .. _create_game([*layout form keywords*]) -> Layout

        *Parameters:*
          All keywords are passed to layout attribute create method.

        *Variables:*
          _layout_ : {Layout}
            layout to call to get coords of stacks.
          _row_ : {Pile}
            stacks used for building cards. Appends to *s['rows']*
          _reserve_ : {Pile}
            stacks used for holding cards. Appends to *s['reserve']*
          _talon_ : {Pile}
            stack used for dealing cards. Appends to *s['talon']*
          _waste_ | _wastes_ : {Pile}
            stack used for holding talon cards. Plural handles common auxilary
            wastes with single main waste. Appends to *s['waste']*
          _foundation_ | _foundations_ : {Pile}
            stack(s) used for finishing cards. Plural handles common split
            finish foundations. (eg 1 set build up and 1 set build down).
            Appends to *s['foundations']*
        """
        if not hasattr(self, '_layout_'):
            raise NotImplementedError
        form, pile = self._layout_(self, **kws), self.s
        form.create(**kws)
        self.table_size = Size(form.size[0], form.size[1])
        kws = {'game': self}
        # Set pile type for single talon.
        cpile, key = getattr(self, '_talon_', None), 'talon'
        assert cpile is not None
        pile[key] = cpile(form.s[key].x, form.s[key].y, **kws)
        # Set pile type sequence for waste. 1st use all but last. 2nd last.
        cpile, key = getattr(self, '_wastes_', None), 'wastes'
        if isinstance(cpile, (list, tuple,)):
            assert getattr(self, '_waste_', None) is None and len(cpile) == 2
            last = len(form.s['wastes']) - 1
            pile[key] = [self._wastes_[int(last == i)](p.x, p.y, **kws)
                            for i, p in enumerate(form.s[key])]
        # Set pile type sequence for foundation. Single suit set per.
        cpile, key = getattr(self, '_foundations_', None), 'foundations'
        if isinstance(cpile, (list, tuple,)):
            assert (getattr(self, '_foundation_', None) is None and
                    len(form.s[key]) / len(cpile) == form.NSUIT)
            pile[key] = [cpile[i // form.NSUIT](p.x, p.y, suit=p.suit, **kws)
                            for i, p in enumerate(form.s[key])]
        # Set pile type for wastes, foundations, rows, and reserves.
        for key in ['wastes', 'foundations', 'rows', 'reserves']:
            cpile = getattr(self, '_{}_'.format(key[:-1]), None) # singular form
            if cpile is not None:
                if not key == 'foundations':
                    pile[key] = [cpile(p.x, p.y, **kws) for p in form.s[key]]
                else:  # Set foundation suit.
                    pile[key] = [cpile(p.x, p.y, suit=p.suit, **kws)
                                  for p in form.s[key]]
        form.defaultAll()
        return form

    @state.not_state('is_preview')
    def _update_text(self):
        """
        Up canvas texts for current.

        Stub to update text items from primarily game texts not pile texts.

        ..\_update_text(self)
        """
        pass

    @property
    def score(self):
        """
        Score of current game.

        Stub to score a game. Scores in patience games don't always make sense.

        .. score -> int
        """
        return 0

    @property
    def _highlight_piles(self):
        """
        Return piles for highlight possible move.

        Use group hp stacks with more than 2 cards for card matching. Else none.

        .. \__highlight_piles -> seq(seq(seq(Pile), int) ...)
        """
        return ((self.sg['hp_stacks'], 2,),) if self.sg['hp_stacks'] else ()

    def _timer_start(self):
        """
        Start timer and reset status time for current game.

        .. \_timer_start() -> None

        *Variables:*
           play_timer: {Event Timer} - set callback for timer update after
             TIMER_UPDATE.
        """
        if not self.top:
            return
        update_console(time=None)
        self._timer_stop()
        self._timer.adjust()
        self.play_timer = self.top.after(TIMER_UPDATE, self._timer_update)

    def _timer_stop(self):
        """
        Stop timer for current game.

        .. \_timer_stop() -> None.

        *Variables:*
           play_timer: {Event Timer} - unset callback timer.
        """
        if not self.top:
            return
        if hasattr(self, 'play_timer') and self.play_timer:
            self.top.after_cancel(self.play_timer)
            self.play_timer = None
            self._timer_update(after=False)

    @state.not_state('is_finish', 'is_pause')
    def _timer_update(self, after=True):
        """
        Update timer and status bar for current game.

        When given do_after, set next callback to call self after TIMER_UPDATE.
        Calls update status method to update time value regardless.

        .. \_timer_update(after:bool=True)

        *Parameters*:
          after : {bool} True
            whether callback is set.

        *Variables:*
           play_timer: {Event Timer} - set callback for self after TIMER_UPDATE.
        """
        timer = str(self._timer)
        if not self.top:
            return
        if after:
            self.play_timer = self.top.after(TIMER_UPDATE, self._timer_update)
        if not self.is_preview:
            update_console(time=timer)

    @state.not_state('is_finish')
    def _timer_pause(self, pause=True):
        """
        Enter or Exit Pause.

          - Enter: hide canvas game items and update timer.
          - Exit: unhide canvas game items and adjust timer before update.

        .. _timer_pause(pause:bool=True) -> None

        *Variables:*
          pause: {boolean}
            pause or unpase based on current state.
        """
        if self.is_pause == pause:
            return
        self.demo_stop()   # Stop demo_mode
        self.is_pause = pause
        if self.is_pause:  # Pause timer and game
            self.top.event_generate('<<update_menus>>')
            self._timer_update(after=False)
            self.canvas.hide('game')
            self.canvas.set_overlay('pause')
        else:              # Resume timer and game
            self._timer.adjust()
            self._timer_update()
            self.canvas.set_overlay()
            self.canvas.show('game')

    @property
    def is_complete(self):
        """
        Return game complete.

        Default s[foundations] contain all cards.

        .. is_complete -> bool
        """
        return bool(sum(len(s.cards)
                    for s in self.s['foundations']) == self['ncards'])

    @property
    def talon(self):
        return self.s['talon']

    def _autopiles(self, event=None):
        """
        Return piles for auto or quick move.

        Return (autoflip, autodrop, quickplay). Default *sg.dropstacks \* 3*.
        Overide to adjust behavior of auto and quick move behavior.

        .. \_autopiles(self, event=None) -> seq(seq(Pile ...), seq(Pile ...),
             seq(Pile ...))
        """
        return ([self.sg['dropstacks']] * 3)


    def _hide_stack_tip(self):
        """
        Remove pile tip items from canvas.

        .. \_hide_stack_tip() -> None
        """
        from ..viewlib.tooltip import PileToolTips
        return PileToolTips.delete()